<?php

require_once('lib/geshi/geshi.php');

/**
 * class DocsParser
 *
 * Parses JS files after scripts.json or file list. Caches
 * results.
 *
 * @todo parseFromSVN directory support
 *
 * @version		prototype
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 * @package		natural-api
 */
class DocsParser
{
	public $path;

	public $cache;

	public $input = '';

	public $lastmodified = 0;

	function DocsParser($path = '../builds/trunk/', $cache = 'cache/trunk')
	{
		$this->path = $path;
		$this->cache = $cache;

		$this->data = array();
	}

	function parseFromJson(array $ignore = array() )
	{
		$dirs = @json_decode(@file_get_contents($this->path . 'scripts.json'), true);

		if (!$dirs)
		{
			throw new Exception("Failed to load scripts.json from {$this->path}");
		}

		$files = array();
		$this->data['files'] = array();

		$source_path = $this->path . 'Source';

		if (!is_dir($source_path) )
		{
			$source_path = $this->path;
		}
		else
		{
			$source_path .= '/';
		}

		foreach ($dirs as $dirname => $dir)
		{
			foreach ($dir as $filename => $deps)
			{
				if (in_array($filename, $ignore)) continue;
				$files[] = $source_path . "$dirname/$filename.js";
				$this->data['files'][$filename .'.js'] = $deps;
			}
		}

		return $this->parseFromFiles($files);
	}

	function parseFromFiles(array $files)
	{
		$this->files = $files;
		$this->lastmodified = 0;

		foreach($files as $file)
		{
			if (!is_readable($file) )
			{
				throw new Exception("File '{$file}' not readable!");
			}

			if (filemtime($file) > $this->lastmodified)
			{
				$this->lastmodified = filemtime($file);
			}
		}

		if ($this->cache && is_readable($this->cache) && filemtime($this->cache) >= $this->lastmodified && filemtime(__FILE__) < filemtime($this->cache))
		{
			$this->data = unserialize(file_get_contents($this->cache) );
		}
		else
		{
			$input = '';
			foreach ($files as $file)
			{
				$input .= file_get_contents($file) . "\n";
			}

			$this->parse($input);

			if ($this->cache)
			{
				file_put_contents($this->cache, serialize($this->data) );
			}

		}

		return $this->data;
	}

	public function output($type = 'json', $property = 'all')
	{
		if (($property === null) || !isset($this->data[$property]) )
		{
			$property = 'all';
		}
		$buffer = $this->data[$property];

		ob_start('ob_gzhandler');

		header("Cache-Control: must-revalidate");
		header("Last-Modified: " . gmdate("D, d M Y H:i:s", $this->lastmodified) . " GMT");
		header("Expires: " . gmdate ("D, d M Y H:i:s", time() + 3600) . " GMT");

		switch ($type)
		{
			case 'dump':
				header("Content-type: text/plain; charset: UTF-8");
				header('Content-Disposition: inline; filename=data.php');
				print_r($buffer);
				break;
			case 'php':
				header("Content-type: text/plain; charset: UTF-8");
				header('Content-Disposition: inline; filename=data.php');
				echo serialize($buffer);
				break;
			default:
				header("Content-type: application/json; charset: UTF-8");
				header('Content-Disposition: inline; filename=data.json');
				echo json_encode($buffer);
		}

		ob_end_flush();
	}


	/**
	 * @todo	2nd level property support, fix for code parts and multiple declared classes
	 */
	public function parse($input)
	{
		preg_match_all('/\/\*[ \n](.*)?\*\//msU', $input, $m, PREG_PATTERN_ORDER);
		$docs = $m[1];

		$this->data = array_merge(array(
			'all' => array(),
			'scripts' => array(),
			'namespaces' => array()
		), $this->data);

		$script = false;
		$namespace = false;

		foreach ($docs as $item)
		{
			if (strpos($item, "\n") === false)
			{
				$match = array_map('trim', explode(':', $item));
				$match = array(
					'keyword' => $match[0],
					'property' => $match[1]
				);
			}
			else
			{
				preg_match('/\t*/m', $item, $m);
				$tabs = strlen($m[0]);
				if (!preg_match_all('/\t{'. $tabs .'}([\w- ]+):(?: ([^\n]+))?\n(\t{'. ($tabs + 1) .'}.*)(?=^\t{'. $tabs .'}[A-Z]|Â§Â§Â§)/msU', $item. 'Â§Â§Â§', $m, PREG_SET_ORDER) )
				{
					throw new Exception("Unparsable section: '{$item}'");
				}
				foreach ($m as $index => $bits)
				{
					$sub = array(
						'keyword'		=> htmlspecialchars($bits[1]),
						'property'		=> null
					);
					if ($bits[2]) $sub['property'] = $bits[2];
					if ($bits[3])
					{
						/**
						 * @todo Parse code sections
						 */
						$sub['description'] = implode("\n", explode("\n" . str_repeat("\t", $tabs + 1), htmlspecialchars(trim($bits[3]) ) ) );
						$sub['description'] = preg_replace('/(\s*)(\S+)\s+-(?:\s+(\(\w+\)))?\s+([^\n]+)/m', '$1<dl><dt><dfn>$2</dfn> <var>$3</var></dt><dd>$4</dd></dl>', $sub['description']);
						$sub['description'] = preg_replace_callback('/(?:^|\n)&gt; ?([^\n]*)|\[([^\]]+)\]\n(.*)\n\[\/\2\]/mUs', array($this, 'parseCode'), $sub['description']);
						$sub['description'] = preg_replace('/(\(\w+\))\s+([^\n]+)/m', '<dl><dt><var>$1</var></dt><dd>$2</dd></dl>', $sub['description']);
						$sub['description'] = preg_replace('/&lt;(http:\/\/\S+)&gt;/iU', '&lt;<a href="$1">$1</a>&gt;', $sub['description']);
						$sub['description'] = str_replace("\n", '<br/>', $sub['description']);
					}
					else
					{
						$sub['description'] = true;
					}
					if (!$index)
					{
						$match = $sub;
					}
					else
					{
						if (!isset($match['properties']))
						{
							$match['properties'] = array();
						}
						$match['properties'][$sub['keyword']] = $sub['description'];
					}
				}
			}
			$property = $match['property'];

			switch($match['keyword'])
			{
				case 'Script':
					$this->data['scripts'][$property] = array();
					$script = $property;
					$namespace = false;
					break;
				case 'Class':
				case 'Native':
				case 'Hash':
				case 'Object':
					if (!isset($this->data['namespaces'][$property]))
					{
						$this->data['namespaces'][$property] = array();
					}
					$namespace = $property;
					break;
				case 'Property':
				case 'Method':
					break;
				default:
					$namespace = false;
			}
			if ($match['keyword'] != 'Script')
			{
				$match['script'] = $script;
			}
			if ($namespace && $namespace != $property)
			{
				$match['namespace'] = $namespace;
			}

			if ($namespace)
			{
				$this->data['namespaces'][$namespace][] = $match;
			}
			if (isset($match['script']))
			{
				$this->data['scripts'][$match['script']][] = $match;
			}
			$this->data['all'][] = $match;
		}

	}

	protected function parseCode($matches)
	{
		$language = 'javascript';
		$source = $matches[1];

		if (!$source && isset($matches[2]) && isset($matches[3]))
		{
			$language = $matches[2];
			$source = $matches[3];
		}

		$geshi = new GeSHi($source, $language);

		return $geshi->parse_code();
	}

}

?>